﻿namespace Microsoft.Formula.Extensions.CodeGenerator
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.ComponentModel.Design;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;

    using EnvDTE;

    using Microsoft.Formula.API;
    using Microsoft.Formula.API.ASTQueries;
    using Microsoft.Formula.API.Nodes;
    using Microsoft.Formula.API.Generators;

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio.OLE.Interop;
    using Microsoft.VisualStudio.Shell;

    using VSLangProj80;

    using MSBuildEvaluation = Microsoft.Build.Evaluation;
    using MSBuildExecution = Microsoft.Build.Execution;

    /// <summary>
    /// Acts as a mediator between the Visual Studio and WPS command line code generators.
    /// </summary>
    [PackageRegistration(UseManagedResourcesOnly = true)]
    // This attribute is used to register the informations needed to show the this package
    // in the Help/About dialog of Visual Studio.
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [ComVisible(true)]
    [Guid(Constants.DefaultGeneratorIdString)]
    [CodeGeneratorRegistration(typeof(FormulaCodeGeneratorPackage), Constants.GeneratorName, vsContextGuids.vsContextGuidVCSProject)]
    [ProvideObject(typeof(FormulaCodeGeneratorPackage))]
    public sealed class FormulaCodeGeneratorPackage : IVsSingleFileGenerator, IObjectWithSite, IDisposable
    {
        /// <summary>Flag to keep track of whether this object has already been disposed.</summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool isDisposed;

        /// <summary>The service provider to use to get services that this generator requires.</summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private ServiceProvider provider;

        /// <summary>The site for this generator to use.</summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private object site;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Dictionary<string, GenerateConfig> configMap = new Dictionary<string, GenerateConfig>();

        /// <summary>The project collection.</summary>
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private MSBuildEvaluation::ProjectCollection projectsCollection = new MSBuildEvaluation.ProjectCollection();

        public FormulaCodeGeneratorPackage()
            : base()
        {
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            if (!this.isDisposed)
            {
                this.isDisposed = true;
                this.provider.Dispose();
                this.site = null;

                GC.SuppressFinalize(this);
            }
        }

        /// <summary>
        /// Retrieves the requested interface.
        /// </summary>
        /// <param name="riid">The ID of the COM interface to retrieve.</param>
        /// <param name="ppvSite">The site.</param>
        void IObjectWithSite.GetSite(ref Guid riid, out IntPtr ppvSite)
        {
            if (site == null)
            {
                throw new COMException("Object is not sited.", VSConstants.E_FAIL);
            }

            IntPtr unknownPointer = Marshal.GetIUnknownForObject(site);
            IntPtr intPointer = IntPtr.Zero;
            Marshal.QueryInterface(unknownPointer, ref riid, out intPointer);

            if (intPointer == IntPtr.Zero)
            {
                throw new COMException("Site does not support requested interface.", VSConstants.E_NOINTERFACE);
            }

            ppvSite = intPointer;
        }

        /// <summary>
        /// Sets the site of this object.
        /// </summary>
        /// <param name="unkSite">The site.</param>
        void IObjectWithSite.SetSite(object unkSite)
        {
            this.site = unkSite;
            this.provider = new ServiceProvider(site as Microsoft.VisualStudio.OLE.Interop.IServiceProvider);
            this.LoadProjectProperties();
        }

        /// <summary>
        /// Retrieves the default extension to use for files generated by this
        /// generator. 
        /// </summary>
        /// <param name="pbstrDefaultExtension">The default extension for files generated by this generator.</param>
        /// <returns>The default generated stub file extension.</returns>
        int IVsSingleFileGenerator.DefaultExtension(out string pbstrDefaultExtension)
        {
            pbstrDefaultExtension = Constants.GeneratedFileExtension;

            if (null != this.provider)
            {
                var projectItem = (ProjectItem)this.provider.GetService(typeof(ProjectItem));

                if (null != projectItem)
                {
                    string itemFullPath = projectItem.FileNames[1];
                    var extension = Path.GetExtension(itemFullPath);

                    if (!String.IsNullOrWhiteSpace(extension))
                    {
                        pbstrDefaultExtension = extension + Constants.GeneratedFileExtension;
                    }
                }
            }
            
            return VSConstants.S_OK;
        }

        /// <summary>
        /// Generates the specified WSZ input file path.
        /// </summary>
        /// <param name="wszInputFilePath">The input file path.</param>
        /// <param name="bstrInputFileContents">The input file contents.</param>
        /// <param name="wszDefaultNamespace">The default namespace.</param>
        /// <param name="outputFileContents">The encoded byte array containing the generated content.</param>
        /// <param name="pcbOutput">The length of the content array <paramref name="outputFileContents"/>.</param>
        /// <param name="pGenerateProgress">The callback to invoke to report on the progress of the generation.</param>
        /// <returns></returns>
        int IVsSingleFileGenerator.Generate(
            string wszInputFilePath,
            string bstrInputFileContents,
            string wszDefaultNamespace,
            IntPtr[] rgbOutputFileContents,
            out uint pcbOutput,
            IVsGeneratorProgress pGenerateProgress)
        {
            pcbOutput = 0;
            ProgressReporter reporter = new ProgressReporter(pGenerateProgress);
            var writer = new StringWriter();
            if (Compile(wszInputFilePath, wszDefaultNamespace, writer, reporter))
            {
                writer.Close();
                EncodeStubOutput(writer.ToString(), rgbOutputFileContents, out pcbOutput);
                return VSConstants.S_OK;
            }
            else
            {
                return VSConstants.E_FAIL;
            }
        }

        /// <summary>
        /// Gets all the properties of the associated project.
        /// </summary>
        /// <returns></returns>
        private void LoadProjectProperties()
        {
            var dteProjectItem = (ProjectItem)this.provider.GetService(typeof(ProjectItem));
            var dteProject = dteProjectItem.ContainingProject;

            var msbProject = this.projectsCollection.LoadProject(dteProject.FullName);
            var pi = msbProject.CreateProjectInstance();

            foreach (var itm in pi.Items)
            {
                try
                {
                    if (itm.GetMetadataValue(Constants.GeneratorProperty) != Constants.GeneratorName)
                    {
                        continue;
                    }

                    var fullPath = Path.Combine(pi.Directory, itm.EvaluatedInclude);
                    var config = new GenerateConfig();
                    var val = itm.GetMetadataValue(Constants.IsThreadSafeProperty);
                    if (val != null && val.ToLowerInvariant() == "false")
                    {
                        config.IsThreadSafe = false;
                    }

                    val = itm.GetMetadataValue(Constants.IsObjectGraphProperty);
                    if (val != null && val.ToLowerInvariant() == "false")
                    {
                        config.IsObjectGraph = false;
                    }

                    val = itm.GetMetadataValue(Constants.IsNewOnlyProperty);
                    if (val != null && val.ToLowerInvariant() == "false")
                    {
                        config.IsNewOnly = false;
                    }

                    configMap[fullPath] = config;
                }
                catch
                {
                }
            }
        }

        public bool Compile(
            string filename, 
            string nameSpace,
            TextWriter writer,
            ProgressReporter reporter)
        {
            GenerateConfig config;
            if (!configMap.TryGetValue(filename, out config))
            {
                config = new GenerateConfig();
            }

            var env = new Env();
            try
            {
                InstallResult ires;
                var progName = new ProgramName(filename);
                env.Install(filename, out ires);
                PrintFlags(ires.Flags, reporter);
                if (!ires.Succeeded)
                {
                    return false;
                }

                AST<API.Nodes.Program> program = null;
                foreach (var touched in ires.Touched)
                {
                    if (touched.Program.Node.Name.Equals(progName))
                    {
                        program = touched.Program;
                        break;
                    }
                }

                if (program == null)
                {
                    reporter.Error(filename, 0, 0, "Could not find input file");
                    return false;
                }

                string name;
                string modName = null;
                var moduleQuery = new NodePred[] { NodePredFactory.Instance.Star, NodePredFactory.Instance.Module };
                program.FindAll(
                    moduleQuery,
                    (ch, n) =>
                    {
                        if (n.TryGetStringAttribute(AttributeKind.Name, out name))
                        {
                            if (modName == null)
                            {
                                modName = name;
                            }
                            else
                            {
                                reporter.Warning(
                                    filename,
                                    n.Span.StartLine,
                                    n.Span.StartCol,
                                    string.Format("Code will only be generated for module {0}; ignoring module {1}.", modName, name));
                            }
                        }
                    });

                if (modName == null)
                {
                    reporter.Warning(filename, 0, 0, "Could not find any modules in input file.");
                    return true;
                }

                var opts = new GeneratorOptions(
                    GeneratorOptions.Language.CSharp,
                    config.IsThreadSafe,
                    config.IsNewOnly,
                    modName,
                    nameSpace);

                Task<GenerateResult> gres;
                env.Generate(progName, modName, writer, opts, out gres);
                gres.Wait();
                PrintFlags(filename, gres.Result.Flags, reporter);
                return gres.Result.Succeeded;
            }
            catch (Exception e)
            {
                reporter.Error(filename, 0, 0, e.Message);
                return false;
            }
        }

        private void PrintFlags(
            string file,
            IEnumerable<Flag> flags,
            ProgressReporter reporter)
        {
            foreach (var f in flags)
            {
                switch (f.Severity)
                {
                    case SeverityKind.Info:
                        reporter.Info(
                            file,
                            f.Span.StartLine,
                            f.Span.StartCol,
                            string.Format("{0} (Code {1})", f.Message, f.Code));
                        break;
                    case SeverityKind.Warning:
                        reporter.Warning(
                            file,
                            f.Span.StartLine,
                            f.Span.StartCol,
                            string.Format("{0} (Code {1})", f.Message, f.Code));
                        break;
                    case SeverityKind.Error:
                        reporter.Error(
                            file,
                            f.Span.StartLine,
                            f.Span.StartCol,
                            string.Format("{0} (Code {1})", f.Message, f.Code));
                        break;
                    default:
                        throw new NotImplementedException();
                }
            }
        }

        private void PrintFlags(
            IEnumerable<Tuple<AST<Microsoft.Formula.API.Nodes.Program>, Flag>> flags,
            ProgressReporter reporter)
        {
            foreach (var f in flags)
            {
                switch (f.Item2.Severity)
                {
                    case SeverityKind.Info:
                        reporter.Info(
                            f.Item1.Node.Name.ToString(),
                            f.Item2.Span.StartLine,
                            f.Item2.Span.StartCol,
                            string.Format("{0} (Code {1})", f.Item2.Message, f.Item2.Code));
                        break;
                    case SeverityKind.Warning:
                        reporter.Warning(
                            f.Item1.Node.Name.ToString(),
                            f.Item2.Span.StartLine,
                            f.Item2.Span.StartCol,
                            string.Format("{0} (Code {1})", f.Item2.Message, f.Item2.Code));
                        break;
                    case SeverityKind.Error:
                        reporter.Error(
                            f.Item1.Node.Name.ToString(),
                            f.Item2.Span.StartLine,
                            f.Item2.Span.StartCol,
                            string.Format("{0} (Code {1})", f.Item2.Message, f.Item2.Code));
                        break;
                    default:
                        throw new NotImplementedException();
                }
            }
        }

        /// <summary>
        /// Encodes the stub output.
        /// </summary>
        /// <param name="outputFileContents">The RGB output file contents.</param>
        /// <param name="pcbOutput">The PCB output.</param>
        private static void EncodeStubOutput(string output, IntPtr[] outputFileContents, out uint pcbOutput)
        {
            Encoding enc = Encoding.UTF8;

            byte[] preamble = enc.GetPreamble();
            int preambleLength = preamble.Length;

            byte[] body = enc.GetBytes(output);

            Array.Resize<byte>(ref preamble, preambleLength + body.Length);
            Array.Copy(body, 0, preamble, preambleLength, body.Length);

            outputFileContents[0] = Marshal.AllocCoTaskMem(preamble.Length);
            Marshal.Copy(preamble, 0, outputFileContents[0], preamble.Length);
            pcbOutput = (uint)preamble.Length;
        }

        private class GenerateConfig
        {
            public bool IsThreadSafe
            {
                get;
                set;
            }

            public bool IsObjectGraph
            {
                get;
                set;
            }

            public bool IsNewOnly
            {
                get;
                set;
            }

            public GenerateConfig()
            {
                IsThreadSafe = true;
                IsObjectGraph = true;
                IsNewOnly = true;
            }
        }
    }
}
